#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
key ::= row-key column-family column-qualifier flag timestamp
flag ::= DELETE_ROW | DELETE_COLUMN_FAMILY | DELETE_CELL | INSERT

DELETE_ROW           ::= 0x00
DELETE_COLUMN_FAMILY ::= 0x01
DELETE_CELL          ::= 0x02
INSERT               ::= 0xFF
'''

import md5
import sys
import time
import struct

from base64 import b64encode, b64decode
from bisect import bisect, insort, bisect_left, bisect_right


def DEBUG(fmt, *arg) : print fmt % arg

#######################################################################

def assembleKey(row_key, column_family, column_qualifier, flag, timestamp) :
    if column_family is None : column_family = 0
    if column_qualifier is None : column_qualifier = ''
    if timestamp is None : timestamp = int(time.time())

    family = struct.pack('!B', column_family + 0x30)
    qualifier = column_qualifier
    _flag = struct.pack('!B', flag)
    _timestamp  = struct.pack('!I', timestamp)

    key = ' '.join((row_key, family, qualifier, _flag, _timestamp))
    return  key
    # assembleKey()

def desembleKey(key) :
    if key is None : return (None,None,None,None,None)

    items = key.split(' ',4)
    row_key = items[0]
    family, = struct.unpack('!B', items[1])
    qualifier = items[2]
    flag, = struct.unpack('!B', items[3])
    timestamp, = struct.unpack('!I', items[4])

    return (row_key, family - 0x30, qualifier, flag, timestamp)
    # desembleKey()


#######################################################################


class   CellCache( object ) :
    ACT_DELETE_ROW           = 0x00
    ACT_DELETE_COLUMN_FAMILY = 0x01
    ACT_DELETE_CELL          = 0x02
    ACT_INSERT               = 0xFF

    def __init__(self) :
        self.__content = {}
        # __init__()

    def load(self, file_handle=None) :
        '''
        load data from file
        '''
        raise NotImplementedError
        # load()

    def dump(self, file_handle=sys.stdout) :
        '''
        dump data into file
        '''
        raise NotImplementedError
        # dump()

    def compact(self) :
        '''
        just compact data in CellCache, to imply DELETE action.
        BUT THIS FUNCTION SHOULD NOT JUST DO ON CACHE, ONLY !!!
        '''
        raise NotImplementedError
        # compact()

    def clear(self) :
        self.__content = {}
        # clear()

    def insert(self, data, row_key, column_family=None, column_qualifier=None, timestamp=None) :
        if not data : return

        key = assembleKey(row_key, column_family, column_qualifier, self.ACT_INSERT, timestamp)
        if row_key in self.__content :
            haystack = self.__content[row_key]
            insort(haystack, (key, data))
        else :
            self.__content[row_key] = [(key,data),]
        # insert()

    def erase(self, row_key, column_family=None, column_qualifier=None, timestamp=None) :
        if row_key not in self.__content :
            return

        if column_family is None :
            flag = self.ACT_DELETE_ROW
        elif column_qualifier is None :
            flag = self.ACT_DELETE_COLUMN_FAMILY
        else :
            flag = self.ACT_DELETE_CELL

        if timestamp is None :
            # delete all records about this key
            timestamp = 0

        key = assembleKey(row_key, column_family, column_qualifier, flag, timestamp)
        haystack = self.__content[row_key]
        insort(haystack, (key, ''))
        # erase()

    def find(self, row_key, column_family=None, column_qualifier=None, timestamp=None) :
        if row_key not in self.__content :
            return None

        if column_family is None :
            flag = self.ACT_DELETE_ROW
        elif column_qualifier is None :
            flag = self.ACT_DELETE_COLUMN_FAMILY
        else :
            flag = self.ACT_DELETE_CELL

        if timestamp is None :
            # find all records about this key
            timestamp = 0

        haystack = self.__content[row_key]

        key_left = assembleKey(row_key, column_family, column_qualifier, flag, timestamp)
        index_left = bisect_left(haystack, (key_left, ''))
        #DEBUG('Index-Left=%d KeyLeft=%s', index_left, `key_left`)

        results = []
        for index in range(index_left, len(haystack)) :
            key, data = haystack[index]
            #DEBUG("RowKey='%s' KEY=%s DATA='%s'", row_key, `key`, data)

            _row_key, _family, _qualifier, _flag, _timestamp = desembleKey(key)
            if _row_key != row_key :
                break
            if self.ACT_DELETE_CELL == _flag :
                # all records sort after this record were deleted
                break

            key_spec = (_row_key, _family, _qualifier, _timestamp)
            results.append((key_spec, data))

        return results if results else None
        # find()

    def scan(self, key_range_spec) :
        raise NotImplementedError
        # scan

    # class CellCache

#######################################################################

def test3() :
    column_family = 7
    column_qualifier = 'tag:test'

    haystack = []
    timestamp = 1000
    for repeats in range(3) :
        for row_key in ('foo','bar','baz') :
            for flag in (0xFF,0x00,0x01,0x00) :
                timestamp += 1
                key = assembleKey(row_key, column_family, column_qualifier, flag, timestamp)
                insort(haystack, key)

    for needle in haystack :
        _row_key, _family, _qualifier, _flag, _timestamp = desembleKey(needle)
        print 'KEY:%s Family:%d Qualifier:%s Flag:0x%02X TimeStamp:%d' % (_row_key, _family, _qualifier, _flag, _timestamp)

    # test3()

def test2() :
    max_row_key = 1048576
    min_row_key = 1

    column_family = 7
    column_qualifier = 'tag:test'
    flag = 0
    timestamp = 0

    # prepare
    keys = []
    for k in xrange(min_row_key, max_row_key + min_row_key, 1) :
        row_key = md5.new(str(k)).hexdigest()
        keys.append( row_key )
    count_records = max_row_key - min_row_key + 1

    keys_encoded = []
    time_start = time.time()
    for row_key in keys :
        key = assembleKey(row_key, column_family, column_qualifier, flag, timestamp)
        keys_encoded.append(key)
    time_stop = time.time()
    time_last = time_stop - time_start
    if 0 == time_last : time_last = 0.001
    tps = count_records / time_last
    DEBUG('ENCODE records:%d  time-last:%.2f seconds  tps:%.2f', count_records, time_last, tps)

    time_start = time.time()
    for key in keys_encoded :
        (_row_key, _family, _qualifier, _flag, _timestamp) = desembleKey(key)
    time_stop = time.time()
    time_last = time_stop - time_start
    if 0 == time_last : time_last = 0.001
    tps = count_records / time_last
    DEBUG('DECODE records:%d  time-last:%.2f seconds  tps:%.2f', count_records, time_last, tps)

    # test2()

def test1() :
    max_row_key = 65536
    min_row_key = 1

    column_family = 7
    column_qualifier = 'tag:test'

    # prepare
    key_values = []
    for k in xrange(min_row_key, max_row_key + min_row_key, 1) :
        row_key = md5.new(str(k)).hexdigest()
        data = b64encode(str(k))
        key_values.append( (row_key, data) )
    count_records = max_row_key - min_row_key + 1

    cache = CellCache()

    time_start = time.time()
    for row_key,data in key_values :
        cache.insert(data, row_key, column_family, column_qualifier)
    time_stop = time.time()
    time_last = time_stop - time_start
    tps_insert = count_records / time_last
    DEBUG('INSERT records:%d  time-last:%.2f seconds  tps:%.2f', count_records, time_last, tps_insert)

    time_start = time.time()
    for row_key,data in key_values :
        resultset = cache.find(row_key, column_family, column_qualifier)
        if not resultset :
            #DEBUG('KEY=%s NOT FOUND', row_key)
            continue
        for result in resultset :
            key_spec, data = result
            _row_key, _family, _qualifier, _timestamp = key_spec
            _data = b64decode(data)
            #DEBUG('FIND KEY=%s RETURN: KEY=%s FAMILY=%d QUALIFIER="%s" TimeStamp=%d', row_key, _row_key, _family, _qualifier, _timestamp)
    time_stop = time.time()
    time_last = time_stop - time_start
    tps_insert = count_records / time_last
    DEBUG('  FIND records:%d  time-last:%.2f seconds  tps:%.2f', count_records, time_last, tps_insert)

    time_start = time.time()
    for row_key,data in key_values :
        cache.erase(row_key, column_family, column_qualifier)
    time_stop = time.time()
    time_last = time_stop - time_start
    tps_insert = count_records / time_last
    DEBUG(' ERASE records:%d  time-last:%.2f seconds  tps:%.2f', count_records, time_last, tps_insert)

    time_start = time.time()
    for row_key,data in key_values :
        resultset = cache.find(row_key, column_family, column_qualifier)
        if not resultset :
            #DEBUG('KEY=%s NOT FOUND', row_key)
            continue
        for result in resultset :
            key_spec, data = result
            _row_key, _family, _qualifier, _timestamp = key_spec
            _data = b64decode(data)
            #DEBUG('FIND KEY=%s RETURN: KEY=%s FAMILY=%d QUALIFIER="%s" TimeStamp=%d', row_key, _row_key, _family, _qualifier, _timestamp)
    time_stop = time.time()
    time_last = time_stop - time_start
    tps_insert = count_records / time_last
    DEBUG('  FIND records:%d  time-last:%.2f seconds  tps:%.2f', count_records, time_last, tps_insert)
    # test1()


#######################################################################

if  __name__ == '__main__' :
    #test1()
    test2()
    #test3()

